/*
 Copyright 2012 <copyright holder> <email>

 Licensed under the Apache License, Version 2.0 (the "License
 );
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

#include "hadrmodel.h"

namespace {

#define CONN_AGAIN "30108"

#define CREATESQL "CREATE TABLE PERSONS (ID INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY, NAME VARCHAR(100), FAMILYNAME VARCHAR(100))"
#define DROPSQL "DROP TABLE PERSONS"
#define GETPERSONSQUERY "SELECT * FROM PERSONS"
#define ADDPERSON "INSERT INTO PERSONS VALUES(default,:name,:familyname)"

	// converts UnicodeString to std::string
	void to_string(std::string &st, const UnicodeString &s) {
		st.clear();
		for (int i = 0; i < s.Length(); i++) {
			wchar_t w = s.c_str()[i];
			st.push_back(w);
		}
	}

	// converts std::string to UnicodeString
	void to_unicode(UnicodeString &s, const std::string &st) {
		s = st.c_str();
	}

	// extracts SQL code (4 or 5 digits) from SQLERR string
	void to_sqlcode(std::string &st, const UnicodeString &s) {
		int start = 0;
		UnicodeString sPos = s;
		st.clear();
		// look for SQL substring
		while (true) {
			sPos = s.SubString(start, 1000);
			int p = sPos.Pos("SQL");
			if (p == 0) {
				break;
			}
			p--;
			// firstly check for 4 digits (ends up with 'N')
			int npos = p + 7;
			if (npos >= s.Length()) {
				break;
			}
			wchar_t w = sPos.c_str()[npos];
			if (w == 'N') {
				// ok, 4 digits error code
				to_string(st, sPos.SubString(p + 4, 4));
				break;
			}
			// secondly look for 5 digits
			npos = p + 8;
			if (npos >= s.Length()) {
				break;
			}
			w = sPos.c_str()[npos];
			if (w != 'N') {
				start = npos;
				// look for next SQL substring (if exists)
				continue;
			}
			// ok, 5 digits
			to_string(st, sPos.SubString(p + 4, 5));
			break;
		}
	}
}

void hadrmodel::connect() {
	if (database != NULL) {
		lastActionStatus = "Already connected";
		return;
	}
	database = new TDatabase(0);
	database->Params->Clear();
	database->DatabaseName = "DB2HAD1";
	database->Params->Add("USER NAME=db2had1");
	database->Params->Add("PASSWORD=db2had1");
	database->KeepConnection = true;
	database->LoginPrompt = false;
	try {
		database->Open();
	}
	catch (EDBEngineError &e) {
		UnicodeString s = e.Message;
		to_string(lastActionStatus, s);
		std::string code;
		to_sqlcode(code, s);
		free(database);
		database = NULL;
		return;
	}
	lastActionStatus = "OK. Connected";
}

// common class for running SQL related commands
class ExecuteCommand {

	virtual void command() = 0;

	bool trans;

	TQuery *query;

protected:
	TQuery *getQ(const char *s) {
		query = new TQuery(model->T());
		query->SQL->Add(s);
		return query;
	}

public:
	ExecuteCommand(hadrmodel * p, bool ptrans) : model(p), trans(ptrans) {
		query = NULL;
	}

	void go() {
		if (model->T() == NULL) {
			model->lastActionStatus = "Not connected";
			return;
		}
		bool tryagain = true;
		bool first = true;
		// loop could be executed twice in case of RECREATE_CONNECTION error
		while (tryagain) {
			tryagain = false;
			try {
				// BDE: autocommit takes place if ExecSQL command is issued
				// without StartTransaction
				if (trans && !model->autocommit) {
					model->T()->StartTransaction();
				}
				command();
				if ((model->T() != NULL) && model->T()->InTransaction) {
					model->T()->Commit();
				}
				model->lastActionStatus = "OK";
			}
			catch (EDBEngineError &e) {
				UnicodeString s = e.Message;
				to_string(model->lastActionStatus, s);
				std::string code;
				to_sqlcode(code, s);
				if (code.compare(CONN_AGAIN) == 0 && first) {
					// run again but only once
					// secondly throws exception
					tryagain = true;
					first = false;
				}
			}
			catch (...) {
				model->lastActionStatus = "SQL non EDBEngineError error";
			}
		} // while
		if (query != NULL) {
			query->Close();
			delete query;
		}

	}

protected:
	hadrmodel * model;
};

class DisconnectCommand : public ExecuteCommand {
	void command() {
		model->T()->Close();
		delete model->T();
		model->database = NULL;
	}

public:
	DisconnectCommand(hadrmodel *p) : ExecuteCommand(p, false) {
	}
};

void hadrmodel::disconnect() {
	DisconnectCommand com(this);
	com.go();
}

class RunCommand : public ExecuteCommand {
	const char *sqlC;

	void command() {
		TQuery *query = getQ(sqlC);
		query->ExecSQL();
	}

public:
	RunCommand(hadrmodel *p, const char *pa) : ExecuteCommand(p, true), sqlC(pa)
	{
	}
};

void hadrmodel::createPersons() {
	RunCommand com(this, CREATESQL);
	com.go();
}

void hadrmodel::dropPersons() {
	RunCommand com(this, DROPSQL);
	com.go();
}

void hadrmodel::autoCommit(bool autop) {
	autocommit = autop;
	lastActionStatus = "OK";
}

class GetPersons : public ExecuteCommand {
	void command() {
		TQuery *query = getQ(GETPERSONSQUERY);
		query->Open();
		query->First();
		// fetch and retrieve result
		while (!query->Eof) {
			std::string name;
			to_string(name, query->FieldByName("NAME")->AsString);
			std::string familyname;
			to_string(familyname, query->FieldByName("FAMILYNAME")->AsString);
			int id = query->FieldByName("ID")->AsInteger;
			res.push_back(person(id, name.c_str(), familyname.c_str()));
			query->Next();
		}
	}

public:
	std::vector<person>res;

	GetPersons(hadrmodel *p) : ExecuteCommand(p, false) {
	}
};

std::vector<person>hadrmodel::getListOfPersons() {
	GetPersons com(this);
	com.go();
	return com.res;
}

class AddPerson : public ExecuteCommand {
	void command() {
		// ADDPERSON with parameter markers
		TQuery *query = getQ(ADDPERSON);
		UnicodeString u;
		to_unicode(u, name);
		query->ParamByName("name")->AsString = u;
		UnicodeString fm;
		to_unicode(fm, familyname);
		query->ParamByName("familyname")->AsString = fm;
		query->ExecSQL();
	}

public:
	const std::string &name;
	const std::string &familyname;

	AddPerson(hadrmodel *p, const std::string &pname,
		const std::string &pfamilyname) : ExecuteCommand(p, true), name(pname),
		familyname(pfamilyname) {
	}
};

void hadrmodel::addPerson(const std::string &name,
	const std::string &familyName) {
	AddPerson com(this, name, familyName);
	com.go();
}

std::string hadrmodel::getConnectionStatus() const {

	if (database == NULL) {
		return "Not connected";
	}
	if (autocommit) {
		return "Connected, autocommit on";
	}
	return "Connected, autocommit off";
}
